Unix网络编程-第3章 套接字编程简介
Unix网络编程-第3章 套接字编程简介
3.2 套接字的地址结构
大多数套接字函数都需要一个指向套接字地址结构的指针作为参数。每个协议族都定义自己的套接字地址结构,这些结构的名字均已sockaddr_
开头,并以对应每个协议族的唯一后缀结尾。
IPv4套接字地址结构:
struct in_addr{
in_addr_t s_addr;
};
struct sockaddr_in{
uint8_t sin_len;
//POSIX规范只需要这个结构中的三个字段
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
字段 | 数据类型 | 说明 |
---|---|---|
s_addr | in_addr_t | 至少32位的无符号整数类型 |
sin_port | in_port_t | 至少16位的无符号整数类型 |
sin_family | sa_family_t | 任何无符号整数类型。在支持长度字段的实现中,通常是一个8位无符号整数,在不支持长度字段中,是一个16位的无符号整数 |
套接字地址结构仅在给定主机上使用:虽然结构中某些字段用在不同主机之间的通信,但是结构本身并不在主机之间传递。
为了让套接字函数能够处理来自任何协议族的套接字地址结构,套接字函数定义的参数中使用指向通用套接字地址结构的指针,使用时再进行类型强制转换
通用套接字地址结构:
struct sockaddr{
uint8_t sa_len; //该字段只在一些Unix实现中有
//SuSv3标准不做要求,Linux实现也不存在该字段
sa_family_t sa_family;
char sa_date[14];
};
IPv6套接字地址结构:
struct in6_addr{
uint8_t s6_addr[16];
};
//如果系统支持套接字地址结构中的长度字段,则SIN6_LEN常值必须定义
#define SIN6_LEN
struct sockaddr_in6{
uint8_t sin6_len;
sa_family_t sin6_family;
in_port_t sin6_port;
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr;
uint32_t sin6_scope_id;
}
新的通用套接字地址结构
struct sockaddr_storage{
uint8_t ss_len;
sa_family_t ss_family;
}
套接字地址结构比较:
3.3 值-结果参数
套接字的地址结构总是以引用形式传递给套接字函数的。
套接字的长度作为一个参数传递给套接字函数时,其传递方式取决于该结构的传递方向。
套接字地址结构可以在两个方向上传递:
从进程到内核。函数:bind、connect、sendto。
这些函数的一个参数是指向套接字地址结构的指针,另一个参数是该结构的整数大小。
从内核到进程。函数:accept、recvfrom、getsockname、getpeername。
这些函数的一个参数是指向套接字地址结构的指针,另一个参数是指向表示该结构大小的整数变量的指针(这种类型的参数称为“值-结果”参数)。
值-结果传参: 当函数被调用时,结构大小是一个值,它告诉内核该结构的大小,这样内核在写该结构时不至于越界。 当函数返回时,结构大小又是一个结果,它告诉进程内核在该结构中究竟存储了多少信息。
当套接字地址结构的长度使用值-结果参数时,如果套接字地址结构是固定长度则从内核返回的值总是那个长度,如果是可变长度,则返回值可能小于该结构的最大长度。
3.4 字节排序函数
主机字节序:
- 小端字节序:将低序字节存储在起始地址
- 大端字节序:将高序字节存储在起始地址
最高有效位:MSB:most significant bit
最低有效位:LSB:least significant bit
术语“小端”和“大端”表示:多个字节值的哪一端(小端或大端)存储在该值的起始地址(低地址)。
网络字节序:大端字节序
网络协议必须指定一个网络字节序。由于历史原因和POSIX规范的规定,套接字地址结构中的某些字段必须按照网络字节序进行维护。
主机字节序和网络字节序之间相互转换使用以下4个函数:
- s视为一个16位的值,例如TCP或UDP的端口号
- l视为一个32位的值,例如IPv4地址
- 主机字节序和网络字节序相同的系统中这四个函数定义为空宏
//主机:host(h)
//网络:network(n)
//短整型:short(s)
//长整型:long(l)
#include <netinet/in.h>
//返回网络字节序的值
uint16_t htons(uint16_t host16bitvalue);
uint32_t htonl(uint32_t host32bitvalue);
//返回主机字节序的值
uint16_t ntohs(uint16_t net16bitvalue);
uint32_t ntohl(uint32_t net32bitvalue);
因特网另一个重要的约定是位序,IPv4首部前32位的位序如下:
3.5 字节操作函数
操作多字节段的函数有两组,它们既不对数据作解释,也不假设数据是以空字节符结束的C字符串。
第一组函数源于4.2BSD,名字以b(表示字节)开头
#include <strings.h> void bzero(void *dest, size_t nbytes); void bcopy(const void *src, void *dest, size_t nbytes); //若相等则返回0,否则为非0 int bcmp(const void *ptr1, const void *ptr2, size_t nbytes);
第二组函数源于ANSI C标准,名字以men(表示内存)开头
#include <string.h> //每个函数的最后一个参数都是长度参数 void *memset(void *dest, int c, size_t len); //memcpy函数的参数顺序与C的赋值语句顺序相同:dest = src void *memcpy(void *dest, const void *src, size_t nbytes); //若相等则返回0,否则 // 看第一个不等字节:ptr1 > ptr2,则返回值大于0,否则返回值小于0 int memcmp(const void *ptr1, const void *ptr2, size_t nbytes);
当源字节串与目标字节串重叠时,bcopy能够正确处理,memcpy的操作结果却不可知,这种情况必须改用ANSI C的memmove函数。
比较操作是假设两个不等字节均为无符号字符(unsigned char)的情况下完成的。
3.6 inet_aton、inet_addr和inet_ntoa函数
功能介绍:在点分十进制数串和它长度为32位的网络字节序二进制值间转换IPv4地址。
#include <arpa/inet.h>
//若字符串有效,则返回1,否则返回0
//如果addrptr指针为空,那么该函数仍然对输入的字符串执行有效性检查,但是不存储任何结果。
int inet_aton(const char *strptr, struct in_addr *addrptr);
//若字符串有效,则返回32位二进制网络字节序的IPv4地址,否则返回INADDR_NONE
//NADDR_NONE常值通常是一个32位均为1的值,这意味着点分十进制数串255.255.255.255不能由该函数处理,因为其二进制值被用来指示函数失败。
in_addr_t inet_addr(const char *strptr);
//返回一个点分十进制数串的指针
char *inet_ntoa(struct in_addr inaddr);
3.7 inet_pton和inet_ntop函数
这两个函数对于IPv4地址和IPv6地址都适用。函数名中p和n分别代表表达(presentation)和数值(numeric)。
#include <arpa/inet.h>
//函数执行成功返回1,表达的格式无效返回0,出错返回-1
int inet_pton(int family, const char *strptr, void *addptr);
//函数执行成功返回指向结果的指针,出错返回NULL
const char *inet_ntop(int family, const void *addptr, char *strptr, size_t len);
//family参数可以是AF_INET,也可以是AF_INET6,如果以不被支持的地址族作为family参数,两个函数就都返回一个错误,并将errno置为EAFNOSUPPORT
总结5个函数
3.8 sock_ntop和相关函数
本书编写的协议无关性函数。函数名以sock_开头。
#include "unp.h"
//成功返回非空指针,出错返回NULL
char *sock_ntop(const struct sockaddr * sockaddr, socklen_t addrlen);
//成功返回0,出错返回-1
int sock_bind_wild(int sockfd, int family);
//若地址为同一协议族且相同,则返回0,反则返回非0
int sock_cmp_addr(const struct sockaddr *sockaddr1,
const struct sockaddr *sockaddr2, socklen_t addrlen);
//若地址为同一协议族且端口相同,则返回0,反则返回非0
int sock_cmp_addr(const struct sockaddr *sockaddr1,
const struct sockaddr *sockaddr2, socklen_t addrlen);
//返回:若为IPv4或IPv6地址则为非负端口号,否则为-1
int sock_get_port(const struct sockaddr *sockaddr, socklen_t addrlen);
//成功返回非空指针,出错返回NULL
char *sock_ntop_host(const struct sockaddr *sockaddr, socklen_t addrlen);
void sock_set_addr(const struct sockaddr *sockaddr,
socklen_t addrlen, void *ptr);
void sock_set_port(const struct sockaddr *sockaddr,
socklen_t addrlen, int port);
void sock_set_wild(sturct sockaddr *sockaddr, socklen_t addrlen);
3.9 readn、writen和readline函数
字节流套接字上的read和write函数所表现的行为不同于通常文件的I/O。字节流套接字上调用read或write输入或输出的字节数可能比请求的数量少,然而这不是出错状态。原因在于:内核中用于套接字的缓冲区可能已经达到极限。此时需要的是调用者再次调用read或write函数,输入或输出剩余的字节。
这个现象在read一个字节流套接字时很常见,但是在write一个字节流时只能在该套接字为非阻塞的前提下才出现。
为了预防万一,不让返回的字节计数值不足,编写了三个函数。
#include "unp.h"
ssize_t readn(int filedes, void *buff, size_t nbytes);
ssize_t written(int filedes, const void *buff, size_t nbytes);
ssize_t readline(int filedes, void *buff, size_t maxlen);